Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom mapping and import settings for bank sync providers #4253

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

matt-fidd
Copy link
Contributor

@matt-fidd matt-fidd commented Jan 28, 2025

$${\color{red}\text{This PR contains a migration, don't connect it to a server you'd like to continue using}}$$

Server PR needs merging first: actualbudget/actual-server#558

Features

  • Adds a new page to manage bank sync
  • Custom mapping of fields provided by sync providers into actual per account with live examples from real data
  • Ability to enable/disable importing pending transactions per-account
  • Ability to enable/disable importing notes per-account

Notes

  • Existing transactions will not be affected unless the notes field is blank and the new mapping brings in data there wasn't before.
  • If you upload an old file, or when this is merged, the mapping functionality will not be available until a new transaction is added via sync as there is no raw data stored against the old transactions to pull the available fields from.
  • There will be another PR in the server to alter how transactions are normalised to ensure that the raw sync data is retained.

Testing

  1. Connect the preview build to a blank server, using Move notes normalisation to the server and expose new bank sync fields actual-server#558
  2. Set up bank sync with your favourite provider, the SimpleFIN test credentials work well but data provided is limited. GoCardless is better to show off the full functionality
  3. Use the bank sync page to edit settings then delete and resync some transactions to see the changes.

Screenshots

image image image

Potential TODOs

Issues

Closes #4138
Closes #3838
Closes #2759
Closes #1467

@actual-github-bot actual-github-bot bot changed the title Custom mapping and import settings for bank sync providers [WIP] Custom mapping and import settings for bank sync providers Jan 28, 2025
Copy link

netlify bot commented Jan 28, 2025

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit a485534
🔍 Latest deploy log https://app.netlify.com/sites/actualbudget/deploys/679a814c5c13ca0008cebaea
😎 Deploy Preview https://deploy-preview-4253.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

github-actions bot commented Jan 28, 2025

Bundle Stats — desktop-client

Hey there, this message comes from a GitHub action that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

Total

Files count Total bundle size % Changed
15 6.7 MB → 6.72 MB (+19.07 kB) +0.28%
Changeset
File Δ Size
src/components/banksync/EditSyncAccount.tsx 🆕 +6.22 kB 0 B → 6.22 kB
src/components/banksync/FieldMapping.tsx 🆕 +3.99 kB 0 B → 3.99 kB
src/components/banksync/index.tsx 🆕 +2.78 kB 0 B → 2.78 kB
src/components/banksync/AccountRow.tsx 🆕 +2.42 kB 0 B → 2.42 kB
home/runner/work/actual/actual/packages/loot-core/src/server/util/custom-sync-mapping.ts 🆕 +1.07 kB 0 B → 1.07 kB
src/components/banksync/AccountsHeader.tsx 🆕 +1.04 kB 0 B → 1.04 kB
src/components/banksync/AccountsList.tsx 🆕 +444 B 0 B → 444 B
src/icons/v1/CreditCard.tsx 🆕 +431 B 0 B → 431 B
src/components/sidebar/PrimaryButtons.tsx 📈 +218 B (+10.75%) 1.98 kB → 2.19 kB
src/components/common/Input.tsx 📈 +90 B (+5.40%) 1.63 kB → 1.72 kB
src/components/FinancesApp.tsx 📈 +232 B (+1.95%) 11.59 kB → 11.82 kB
src/components/Modals.tsx 📈 +159 B (+0.91%) 17.04 kB → 17.2 kB
home/runner/work/actual/actual/packages/loot-core/src/client/accounts/accountsSlice.ts 📈 +27 B (+0.52%) 5.07 kB → 5.1 kB
node_modules/debounce/index.js 📈 +4 B (+0.21%) 1.87 kB → 1.88 kB
node_modules/lodash/debounce.js 📉 -4 B (-0.07%) 6 kB → 6 kB
View detailed bundle breakdown

Added

No assets were added

Removed

No assets were removed

Bigger

Asset File Size % Changed
static/js/index.js 4.29 MB → 4.31 MB (+19.54 kB) +0.45%

Smaller

Asset File Size % Changed
static/js/ReportRouter.js 1.59 MB → 1.59 MB (-485 B) -0.03%

Unchanged

Asset File Size % Changed
static/js/indexeddb-main-thread-worker-e59fee74.js 13.5 kB 0%
static/js/workbox-window.prod.es5.js 5.69 kB 0%
static/js/resize-observer.js 18.37 kB 0%
static/js/BackgroundImage.js 122.29 kB 0%
static/js/en-GB.js 93.42 kB 0%
static/js/pt-BR.js 103.29 kB 0%
static/js/uk.js 111.11 kB 0%
static/js/nl.js 79.76 kB 0%
static/js/en.js 99.34 kB 0%
static/js/narrow.js 84.94 kB 0%
static/js/useAccountPreviewTransactions.js 1.69 kB 0%
static/js/AppliedFilters.js 10.52 kB 0%
static/js/wide.js 102.8 kB 0%

Copy link
Contributor

github-actions bot commented Jan 28, 2025

Bundle Stats — loot-core

Hey there, this message comes from a GitHub action that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

Total

Files count Total bundle size % Changed
1 1.33 MB → 1.33 MB (+1.04 kB) +0.08%
Changeset
File Δ Size
packages/loot-core/src/server/util/custom-sync-mapping.ts 🆕 +1.33 kB 0 B → 1.33 kB
packages/loot-core/src/server/accounts/sync.ts 📈 +1007 B (+3.75%) 26.19 kB → 27.18 kB
packages/loot-core/src/server/aql/schema/index.ts 📈 +70 B (+0.46%) 14.71 kB → 14.78 kB
packages/loot-core/src/server/main.ts 📈 +195 B (+0.27%) 69.5 kB → 69.69 kB
View detailed bundle breakdown

Added

No assets were added

Removed

No assets were removed

Bigger

Asset File Size % Changed
kcab.worker.js 1.33 MB → 1.33 MB (+1.04 kB) +0.08%

Smaller

No assets were smaller

Unchanged

No assets were unchanged

@matt-fidd matt-fidd force-pushed the bank-sync branch 2 times, most recently from f37d495 to 5118830 Compare January 28, 2025 22:05
@matt-fidd
Copy link
Contributor Author

/update-vrt

@matt-fidd matt-fidd changed the title [WIP] Custom mapping and import settings for bank sync providers Custom mapping and import settings for bank sync providers Jan 28, 2025
Copy link
Contributor

coderabbitai bot commented Jan 28, 2025

Walkthrough

The pull request introduces a comprehensive bank synchronization feature for the Actual Budget application. The changes span multiple packages, including desktop-client, loot-core, and involve creating new components, updating routing, and enhancing database schemas. The implementation adds a new /bank-sync route, introduces components for managing bank accounts, and provides functionality for configuring transaction mappings and synchronization settings. The changes include UI components for displaying and editing synchronized accounts, database migrations to support new fields, and backend logic for handling bank transaction synchronization.

Assessment against linked issues

Objective Addressed Explanation
Ability to ignore uncleared transactions [#4138] Partial implementation; added transaction processing logic but no explicit uncleared transaction filtering mechanism
Configurable GoCardLess imports [#3838] Introduced FieldMapping component and custom sync mapping utilities
Read remittanceInformationStructured [#2759] Added mapping infrastructure but no specific implementation for structured remittance information
Custom mapping GoCardless fields [#1467] Implemented custom-sync-mapping.ts with flexible mapping capabilities

Possibly related PRs

Suggested labels

bank-sync, feature, enhancement

Suggested reviewers

  • youngcw
  • @actualbudget/maintainers

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (21)
packages/loot-core/src/server/accounts/sync.ts (4)

355-356: Skipping non-cleared transactions
If importPending is false, non-cleared transactions are skipped altogether. Consider how these pending transactions might be handled if the user later enables pending imports.


369-369: Confirm the strict requirement for a 'date'
Throwing an error if date is missing halts the entire sync. Consider skipping or partially applying such transactions instead of stopping the process.


377-377: Ensure empty strings are replaced
Using || payeeName won't replace trans.imported_payee if it's an empty string. A trim-based fallback may help unify the logic.


389-389: Assess storing raw synced data
raw_synced_data may include personal or sensitive data. Consider encrypting it, partially redacting it, or limiting its size to avoid potential data exposure or excessive bloat.
[security]

Also applies to: 394-394, 395-395, 400-400

packages/loot-core/src/types/models/account.d.ts (1)

21-21: Consider using a typed date
If last_sync is meant for date operations, storing it as a string may limit type-safety. Evaluate using an ISO datetime or a dedicated date type.

packages/loot-core/src/types/models/transaction.d.ts (1)

35-35: Simplify the type definition.

The type definition can be simplified since the undefined type is redundant for optional properties.

-  raw_synced_data?: string | undefined;
+  raw_synced_data?: string;
packages/loot-core/src/server/util/custom-sync-mapping.ts (1)

21-38: Refactor default mappings to reduce duplication.

The mappings for 'payment' and 'deposit' are identical. Consider extracting the common mappings to reduce duplication.

+const commonFieldMappings = new Map([
+  ['date', 'date'],
+  ['payee', 'payeeName'],
+  ['notes', 'remittanceInformationUnstructured'],
+]);
+
 export const defaultMappings: Mappings = new Map([
-  [
-    'payment',
-    new Map([
-      ['date', 'date'],
-      ['payee', 'payeeName'],
-      ['notes', 'remittanceInformationUnstructured'],
-    ]),
-  ],
-  [
-    'deposit',
-    new Map([
-      ['date', 'date'],
-      ['payee', 'payeeName'],
-      ['notes', 'remittanceInformationUnstructured'],
-    ]),
-  ],
+  ['payment', new Map(commonFieldMappings)],
+  ['deposit', new Map(commonFieldMappings)],
 ]);
packages/desktop-client/src/components/banksync/AccountsHeader.tsx (2)

14-14: Remove empty style object.

The empty style object on TableHeader is unnecessary.

-    <TableHeader style={{}}>
+    <TableHeader>

15-34: Enhance accessibility and reduce style duplication.

The table header cells need aria labels for better accessibility. Also, the padding style is duplicated across cells.

+const cellStyle = { paddingLeft: '10px' };
+
 <Cell
   value={t('Account')}
   width={!unlinked ? 250 : 'flex'}
-  style={{ paddingLeft: '10px' }}
+  style={cellStyle}
+  aria-label={t('Account column header')}
 />
 {!unlinked && (
   <>
     <Cell
       value={t('Bank')}
       width="flex"
-      style={{ paddingLeft: '10px' }}
+      style={cellStyle}
+      aria-label={t('Bank column header')}
     />
     <Cell
       value={t('Last sync')}
       width={160}
-      style={{ paddingLeft: '10px' }}
+      style={cellStyle}
+      aria-label={t('Last sync column header')}
     />
-    <Cell value="" width={100} style={{ paddingLeft: '10px' }} />
+    <Cell value="" width={100} style={cellStyle} />
   </>
 )}
packages/desktop-client/src/components/banksync/AccountsList.tsx (2)

9-14: Use literal union type for action.

The action type can be more strictly typed using a literal union type.

+type AccountAction = 'link' | 'edit';
+
 type AccountsListProps = {
   accounts: AccountEntity[];
   hoveredAccount?: string | undefined;
   onHover: (id: AccountEntity['id']) => void;
-  onAction: (account: AccountEntity, action: 'link' | 'edit') => void;
+  onAction: (account: AccountEntity, action: AccountAction) => void;
 };

28-40: Optimize performance for large lists.

For better performance with large account lists, consider memoizing the AccountRow components and callback functions.

+import React, { useCallback, useMemo } from 'react';
+
+const MemoizedAccountRow = React.memo(AccountRow);
+
 export function AccountsList({
   accounts,
   hoveredAccount,
   onHover,
   onAction,
 }: AccountsListProps) {
+  const handleHover = useCallback(onHover, [onHover]);
+  const handleAction = useCallback(onAction, [onAction]);
+
   if (accounts.length === 0) {
     return null;
   }
+
+  const accountRows = useMemo(
+    () =>
+      accounts.map(account => {
+        const hovered = hoveredAccount === account.id;
+        return (
+          <MemoizedAccountRow
+            key={account.id}
+            account={account}
+            hovered={hovered}
+            onHover={handleHover}
+            onAction={handleAction}
+          />
+        );
+      }),
+    [accounts, hoveredAccount, handleHover, handleAction],
+  );

   return (
     <View>
-      {accounts.map(account => {
-        const hovered = hoveredAccount === account.id;
-
-        return (
-          <AccountRow
-            key={account.id}
-            account={account}
-            hovered={hovered}
-            onHover={onHover}
-            onAction={onAction}
-          />
-        );
-      })}
+      {accountRows}
     </View>
   );
packages/loot-core/src/types/prefs.d.ts (1)

31-33: LGTM! Consider adding JSDoc comments.

The new preference types follow the existing pattern and naming conventions. To improve maintainability, consider adding JSDoc comments explaining the purpose of each preference type.

+  /** Custom field mappings for bank sync providers */
   | `custom-sync-mappings-${string}`
+  /** Controls import of pending transactions per account */
   | `sync-import-pending-${string}`
+  /** Controls import of transaction notes per account */
   | `sync-import-notes-${string}`
packages/desktop-client/src/components/banksync/AccountRow.tsx (2)

13-20: Consider moving tsToString to a shared utility module.

This timestamp formatting utility could be useful in other components. Consider moving it to a shared utility module for reusability.


47-48: Use optional chaining for event handlers.

The static analysis tool correctly identifies potential null reference issues. Use optional chaining for cleaner code.

-        onMouseEnter={() => onHover && onHover(account.id)}
-        onMouseLeave={() => onHover && onHover(null)}
+        onMouseEnter={() => onHover?.(account.id)}
+        onMouseLeave={() => onHover?.(null)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 47-47: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 48-48: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

packages/desktop-client/src/components/banksync/index.tsx (3)

24-34: Consider making provider list more maintainable.

The provider names are currently hardcoded in the hook. Consider moving these to a configuration file or deriving them from the BankSyncProviders type to make future additions easier.

+import { type BankSyncProviders } from 'loot-core/types/models';
+
+const SYNC_PROVIDERS: Record<BankSyncProviders, string> = {
+  goCardless: 'GoCardless',
+  simpleFin: 'SimpleFIN',
+};
+
 const useSyncSourceReadable = () => {
   const { t } = useTranslation();
 
   const syncSourceReadable: Record<SyncProviders, string> = {
-    goCardless: 'GoCardless',
-    simpleFin: 'SimpleFIN',
+    ...SYNC_PROVIDERS,
     unlinked: t('Unlinked'),
   };

48-74: Optimize memoization of grouped accounts.

The groupedAccounts memo could be optimized by extracting the reducer function and memoizing it separately.

+const useAccountGrouping = () => {
+  const groupAccounts = useCallback((accounts: AccountEntity[]) => {
+    return accounts
+      .filter(a => !a.closed)
+      .reduce(
+        (acc, a) => {
+          const syncSource = a.account_sync_source ?? 'unlinked';
+          acc[syncSource] = acc[syncSource] || [];
+          acc[syncSource].push(a);
+          return acc;
+        },
+        {} as Record<SyncProviders, AccountEntity[]>,
+      );
+  }, []);
+
+  return { groupAccounts };
+};

 export function BankSync() {
+  const { groupAccounts } = useAccountGrouping();
   const groupedAccounts = useMemo(() => {
-    const unsorted = accounts
-      .filter(a => !a.closed)
-      .reduce(
-        (acc, a) => {
-          const syncSource = a.account_sync_source ?? 'unlinked';
-          acc[syncSource] = acc[syncSource] || [];
-          acc[syncSource].push(a);
-          return acc;
-        },
-        {} as Record<SyncProviders, AccountEntity[]>,
-      );
+    const unsorted = groupAccounts(accounts);

93-95: Memoize the onHover callback.

The onHover callback should be memoized to prevent unnecessary re-renders of child components.

-  const onHover = useCallback((id: AccountEntity['id']) => {
+  const onHover = useCallback((id: AccountEntity['id']) => {
     setHoveredAccount(id);
-  }, []);
+  }, [setHoveredAccount]);
packages/desktop-client/src/components/banksync/FieldMapping.tsx (1)

16-31: Extract transaction direction options to constants.

Consider moving the transaction direction options to a separate constants file for better maintainability.

+const TRANSACTION_DIRECTIONS = {
+  PAYMENT: 'payment',
+  DEPOSIT: 'deposit',
+} as const;
+
+type TransactionDirectionType = typeof TRANSACTION_DIRECTIONS[keyof typeof TRANSACTION_DIRECTIONS];
+
 const useTransactionDirectionOptions = () => {
   const { t } = useTranslation();
 
   const transactionDirectionOptions = [
     {
-      value: 'payment',
+      value: TRANSACTION_DIRECTIONS.PAYMENT,
       label: t('Payment'),
     },
     {
-      value: 'deposit',
+      value: TRANSACTION_DIRECTIONS.DEPOSIT,
       label: t('Deposit'),
     },
   ];
packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (2)

42-66: Extract mappable fields configuration.

Consider moving the mappable fields configuration to a separate file for better maintainability and reusability.

+// mappableFieldsConfig.ts
+export const MAPPABLE_FIELDS_CONFIG: MappableField[] = [
+  {
+    actualField: 'date',
+    syncFields: ['date', 'bookingDate', 'valueDate'],
+  },
+  // ... other fields
+];

-const mappableFields: MappableField[] = [
-  {
-    actualField: 'date',
-    syncFields: ['date', 'bookingDate', 'valueDate'],
-  },
-  // ... rest of the fields
-];
+import { MAPPABLE_FIELDS_CONFIG } from './mappableFieldsConfig';
+
+const mappableFields = MAPPABLE_FIELDS_CONFIG;

86-93: Extract preference management to custom hook.

Consider creating a custom hook to manage related preferences, improving code organization and reusability.

+const useAccountPreferences = (accountId: string) => {
+  const [savedMappings, setSavedMappings] = useSyncedPref(
+    `custom-sync-mappings-${accountId}`,
+    mappingsToString(defaultMappings)
+  );
+  const [savedImportNotes, setSavedImportNotes] = useSyncedPref(
+    `sync-import-notes-${accountId}`,
+    true
+  );
+  const [savedImportPending, setSavedImportPending] = useSyncedPref(
+    `sync-import-pending-${accountId}`,
+    true
+  );
+
+  return {
+    savedMappings,
+    setSavedMappings,
+    savedImportNotes,
+    setSavedImportNotes,
+    savedImportPending,
+    setSavedImportPending,
+  };
+};
packages/loot-core/src/server/main.ts (1)

1064-1066: Consider using ISO string for better timestamp readability.

While storing milliseconds since epoch works, using an ISO string format would make the timestamps more readable in the database while maintaining sortability.

-const ts = new Date().getTime().toString();
+const ts = new Date().toISOString();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1c3b9b and a4d6565.

⛔ Files ignored due to path filters (26)
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-4-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-5-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-6-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-13-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-14-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-15-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png is excluded by !**/*.png
  • packages/loot-core/src/server/__snapshots__/main.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap is excluded by !**/*.snap
  • upcoming-release-notes/4253.md is excluded by !**/*.md
📒 Files selected for processing (22)
  • packages/desktop-client/src/components/FinancesApp.tsx (2 hunks)
  • packages/desktop-client/src/components/Modals.tsx (2 hunks)
  • packages/desktop-client/src/components/banksync/AccountRow.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/AccountsHeader.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/AccountsList.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/FieldMapping.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/index.tsx (1 hunks)
  • packages/desktop-client/src/components/common/Input.tsx (1 hunks)
  • packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx (3 hunks)
  • packages/loot-core/migrations/1738072673000_bank_sync_page.sql (1 hunks)
  • packages/loot-core/src/client/accounts/accountsSlice.ts (1 hunks)
  • packages/loot-core/src/client/state-types/modals.d.ts (1 hunks)
  • packages/loot-core/src/mocks/index.ts (3 hunks)
  • packages/loot-core/src/server/accounts/sync.ts (3 hunks)
  • packages/loot-core/src/server/aql/schema/index.ts (2 hunks)
  • packages/loot-core/src/server/main.ts (4 hunks)
  • packages/loot-core/src/server/util/custom-sync-mapping.ts (1 hunks)
  • packages/loot-core/src/types/models/account.d.ts (1 hunks)
  • packages/loot-core/src/types/models/bank-sync.d.ts (1 hunks)
  • packages/loot-core/src/types/models/transaction.d.ts (1 hunks)
  • packages/loot-core/src/types/prefs.d.ts (1 hunks)
🧰 Additional context used
📓 Learnings (6)
packages/loot-core/src/types/models/bank-sync.d.ts (1)
Learnt from: matt-fidd
PR: actualbudget/actual#3581
File: packages/loot-core/src/types/models/bank-sync.d.ts:11-21
Timestamp: 2024-11-10T16:45:25.627Z
Learning: In `packages/loot-core/src/types/models/bank-sync.d.ts`, when defining TypeScript types for data received from the server, maintain the field names as they are in the server response, even if they don't follow TypeScript naming conventions.
packages/loot-core/src/types/models/account.d.ts (1)
Learnt from: matt-fidd
PR: actualbudget/actual#3581
File: packages/loot-core/src/types/models/bank-sync.d.ts:11-21
Timestamp: 2024-11-10T16:45:25.627Z
Learning: In `packages/loot-core/src/types/models/bank-sync.d.ts`, when defining TypeScript types for data received from the server, maintain the field names as they are in the server response, even if they don't follow TypeScript naming conventions.
packages/loot-core/src/server/aql/schema/index.ts (1)
Learnt from: matt-fidd
PR: actualbudget/actual#4141
File: packages/loot-core/src/server/aql/schema/index.ts:145-145
Timestamp: 2025-01-12T20:26:15.249Z
Learning: In the schema definitions in `packages/loot-core/src/server/aql/schema/index.ts`, field definitions should follow the existing pattern using only the common properties (`type`, `default`, `required`, `ref`) for consistency.
packages/desktop-client/src/components/banksync/AccountsList.tsx (1)
Learnt from: joel-jeremy
PR: actualbudget/actual#3685
File: packages/desktop-client/src/components/accounts/Account.tsx:655-665
Timestamp: 2024-11-10T16:45:31.225Z
Learning: The Account component in 'packages/desktop-client/src/components/accounts/Account.tsx' is being rewritten in a separate PR.
packages/desktop-client/src/components/banksync/AccountsHeader.tsx (1)
Learnt from: joel-jeremy
PR: actualbudget/actual#3685
File: packages/desktop-client/src/components/accounts/Account.tsx:655-665
Timestamp: 2024-11-10T16:45:31.225Z
Learning: The Account component in 'packages/desktop-client/src/components/accounts/Account.tsx' is being rewritten in a separate PR.
packages/desktop-client/src/components/banksync/AccountRow.tsx (1)
Learnt from: joel-jeremy
PR: actualbudget/actual#3685
File: packages/desktop-client/src/components/accounts/Account.tsx:655-665
Timestamp: 2024-11-10T16:45:31.225Z
Learning: The Account component in 'packages/desktop-client/src/components/accounts/Account.tsx' is being rewritten in a separate PR.
🪛 Biome (1.9.4)
packages/desktop-client/src/components/banksync/AccountRow.tsx

[error] 47-47: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 48-48: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (25)
packages/loot-core/src/server/accounts/sync.ts (8)

29-32: Looks good
These new imports for custom-sync-mapping appear clean and align with the usage in normalizeBankSyncTransactions.


329-346: Consider stricter validation of preferences for safer defaults
Currently, if the retrieved value is unexpected or absent, the code defaults to 'true'. Make sure that's the intended behavior for every scenario, or handle invalid strings more robustly.


347-350: Handle malformed mapping strings
If customMappingsRaw is corrupted, mappingsFromString may fail or yield inconsistent mappings. Consider adding error handling or a safer fallback to defaultMappings.


353-354: Confirm alignment with user expectations for 'cleared'
Marking every transaction as cleared if booked is truthy might override any manually set states. Double-check that this is the desired logic.


361-361: Check for zero-amount edge case
Here, zero-amount transactions are treated as deposits. Verify that the mapping keys for 'deposit' and 'payment' exist and make sense for such edge cases.


362-366: Validate dynamic mapping fields
Relying on mapping.get('date'), etc., is flexible, but ensure the user-specified mapping keys exist. Otherwise, critical fields (like the date) could be undefined.


373-373: Consider a fallback payee
Some transactions may legitimately lack a payee. If payeeName is null, consider using a placeholder or skipping them to avoid blocking the sync.


386-386: Resolve payees concurrency
This runs within a loop and relies on payeesToCreate to avoid duplicates. Confirm no race conditions occur in parallel code or multi-transaction syncs.

packages/loot-core/src/types/models/bank-sync.d.ts (1)

23-23: Foresee extensibility
Defining BankSyncProviders as a union is straightforward; just remember to extend it if more providers are introduced.

packages/loot-core/src/types/models/account.d.ts (1)

13-14: Carefully handle null bank fields
When T is false, bankName and bankId both become null. Verify that the UI and DB logic gracefully handle those null cases.

packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx (1)

9-9: LGTM! Bank Sync navigation is well integrated.

The Bank Sync menu item is appropriately placed in the sidebar navigation, with a suitable icon choice and correct route configuration.

Also applies to: 30-30, 67-72

packages/desktop-client/src/components/common/Input.tsx (1)

57-59: LGTM! Good visual feedback for disabled state.

The conditional color styling provides clear visual feedback when inputs are disabled, using appropriate theme variables.

packages/desktop-client/src/components/banksync/AccountRow.tsx (1)

22-27: LGTM! Props interface is well-defined.

The TypeScript interface for props is clear and type-safe, using appropriate types from the core package.

packages/desktop-client/src/components/banksync/index.tsx (1)

1-23: LGTM! Well-organized imports and type definitions.

The imports are properly structured, and the type definitions are clear and purposeful.

packages/loot-core/src/mocks/index.ts (1)

54-62: LGTM! Consistent null initialization.

The emptySyncFields function properly initializes all sync-related fields to null.

packages/loot-core/src/client/accounts/accountsSlice.ts (1)

193-194: LGTM! Ensures account data is refreshed after sync.

The addition of dispatch(getAccounts()); improves data consistency by refreshing account data after handling sync responses.

packages/loot-core/src/client/state-types/modals.d.ts (1)

189-193: LGTM! Well-defined modal type for editing synced accounts.

The new modal type is properly integrated and follows TypeScript best practices.

packages/desktop-client/src/components/FinancesApp.tsx (1)

27-27: LGTM! Clean integration of bank sync feature.

The bank sync feature is properly integrated with a new route and follows React Router patterns.

Also applies to: 252-252

packages/loot-core/src/server/aql/schema/index.ts (1)

56-56: LGTM! Schema fields follow consistent pattern.

The new fields raw_synced_data and last_sync are properly defined following the existing schema pattern, aligning with the established conventions.

Also applies to: 77-77

packages/desktop-client/src/components/Modals.tsx (2)

14-14: LGTM!

The import statement follows the project's import style and is used in the new modal case.


388-390: LGTM!

The new modal case follows the established pattern, includes proper React key, and correctly passes the account prop.

packages/loot-core/src/server/main.ts (1)

Line range hint 1048-1063: LGTM!

The function is correctly marked as async to support awaiting database operations while maintaining the same interface.

packages/loot-core/migrations/1738072673000_bank_sync_page.sql (3)

1-2: LGTM!

The migration is properly wrapped in a transaction to ensure atomicity of the schema changes.

Also applies to: 7-7


3-3: LGTM!

The last_sync column is appropriately added with text type and no default value.


5-5: LGTM!

The raw_synced_data column is appropriately added with text type and no default value.

@matt-fidd matt-fidd force-pushed the bank-sync branch 3 times, most recently from ec59e51 to 3dc37f1 Compare January 28, 2025 23:28
@matt-fidd
Copy link
Contributor Author

@coderabbitai full review

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (20)
packages/desktop-client/src/components/banksync/index.tsx (2)

1-23: Consider code splitting or lazy-loading for improved performance.

The comprehensive import list is fine, but if the bundle grows significantly, you may consider code-splitting or lazy-loading less frequently used components, especially for modals or config data, to optimize performance.


24-34: Localize all sync provider strings.

While "Unlinked" is localized via t(...), strings like "GoCardless" and "SimpleFIN" are currently hardcoded. Consider wrapping them in t(...) for consistent localization support.

-    goCardless: 'GoCardless',
-    simpleFin: 'SimpleFIN',
+    goCardless: t('GoCardless'),
+    simpleFin: t('SimpleFIN'),
packages/desktop-client/src/components/banksync/AccountsHeader.tsx (2)

14-14: Remove unnecessary empty style object.

The empty style object style={{}} on TableHeader serves no purpose and can be removed.

-    <TableHeader style={{}}>
+    <TableHeader>

15-33: Consider extracting hardcoded values to constants.

The component has multiple instances of hardcoded padding and width values. Consider extracting these to theme constants or a configuration object for better maintainability and consistency.

+const CELL_STYLES = {
+  padding: { paddingLeft: '10px' },
+  widths: {
+    account: 250,
+    lastSync: 160,
+    actions: 100,
+  },
+} as const;

 export function AccountsHeader({ unlinked }: AccountsHeaderProps) {
   const { t } = useTranslation();

   return (
     <TableHeader>
       <Cell
         value={t('Account')}
-        width={!unlinked ? 250 : 'flex'}
+        width={!unlinked ? CELL_STYLES.widths.account : 'flex'}
-        style={{ paddingLeft: '10px' }}
+        style={CELL_STYLES.padding}
       />
       {!unlinked && (
         <>
           <Cell
             value={t('Bank')}
             width="flex"
-            style={{ paddingLeft: '10px' }}
+            style={CELL_STYLES.padding}
           />
           <Cell
             value={t('Last sync')}
-            width={160}
-            style={{ paddingLeft: '10px' }}
+            width={CELL_STYLES.widths.lastSync}
+            style={CELL_STYLES.padding}
           />
-          <Cell value="" width={100} style={{ paddingLeft: '10px' }} />
+          <Cell value="" width={CELL_STYLES.widths.actions} style={CELL_STYLES.padding} />
         </>
       )}
     </TableHeader>
   );
 }
packages/desktop-client/src/components/banksync/AccountsList.tsx (1)

16-43: Consider memoizing callbacks and mapped components for performance.

While the implementation is clean, consider these performance optimizations:

  1. Memoize the onHover and onAction callbacks using useCallback
  2. Memoize the mapped AccountRow components using useMemo
+import React, { useCallback, useMemo } from 'react';
 
 export function AccountsList({
   accounts,
   hoveredAccount,
   onHover,
   onAction,
 }: AccountsListProps) {
   if (accounts.length === 0) {
     return null;
   }
 
+  const memoizedOnHover = useCallback(onHover, [onHover]);
+  const memoizedOnAction = useCallback(onAction, [onAction]);
+
+  const accountRows = useMemo(
+    () =>
+      accounts.map(account => {
+        const hovered = hoveredAccount === account.id;
+
+        return (
+          <AccountRow
+            key={account.id}
+            account={account}
+            hovered={hovered}
+            onHover={memoizedOnHover}
+            onAction={memoizedOnAction}
+          />
+        );
+      }),
+    [accounts, hoveredAccount, memoizedOnHover, memoizedOnAction]
+  );
 
   return (
     <View>
-      {accounts.map(account => {
-        const hovered = hoveredAccount === account.id;
-
-        return (
-          <AccountRow
-            key={account.id}
-            account={account}
-            hovered={hovered}
-            onHover={onHover}
-            onAction={onAction}
-          />
-        );
-      })}
+      {accountRows}
     </View>
   );
 }
packages/loot-core/src/server/util/custom-sync-mapping.ts (2)

1-29: Enhance type safety and error handling.

While the implementation is good, consider these improvements:

  1. Add type guards for better type safety
  2. Add validation for the inner map values
  3. Make error messages more specific
+type JsonMapping = Record<string, Record<string, string>>;
+
+function isValidInnerMapping(value: unknown): value is Record<string, string> {
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    Object.entries(value).every(
+      ([k, v]) => typeof k === 'string' && typeof v === 'string'
+    )
+  );
+}
+
 export const mappingsFromString = (str: string): Mappings => {
   try {
     const parsed = JSON.parse(str);
     if (typeof parsed !== 'object' || parsed === null) {
-      throw new Error('Invalid mapping format');
+      throw new Error('Mapping must be a JSON object');
     }
+    
+    for (const [key, value] of Object.entries(parsed)) {
+      if (!isValidInnerMapping(value)) {
+        throw new Error(
+          `Invalid inner mapping for key "${key}": all values must be strings`
+        );
+      }
+    }
+
     return new Map(
-      Object.entries(parsed).map(([key, value]) => [
+      Object.entries(parsed as JsonMapping).map(([key, value]) => [
         key,
         new Map(Object.entries(value as object)),
       ]),
     );
   } catch (e) {
     const message = e instanceof Error ? e.message : e;
-    throw new Error(`Failed to parse mapping: ${message}`);
+    throw new Error(
+      e instanceof SyntaxError
+        ? 'Invalid JSON format in mapping string'
+        : `Failed to parse mapping: ${message}`
+    );
   }
 };

31-48: Reduce duplication in default mappings.

The mappings for 'payment' and 'deposit' are identical. Consider extracting the common mapping to reduce duplication.

+const DEFAULT_FIELD_MAPPING = new Map([
+  ['date', 'date'],
+  ['payee', 'payeeName'],
+  ['notes', 'remittanceInformationUnstructured'],
+]);
+
 export const defaultMappings: Mappings = new Map([
-  [
-    'payment',
-    new Map([
-      ['date', 'date'],
-      ['payee', 'payeeName'],
-      ['notes', 'remittanceInformationUnstructured'],
-    ]),
-  ],
-  [
-    'deposit',
-    new Map([
-      ['date', 'date'],
-      ['payee', 'payeeName'],
-      ['notes', 'remittanceInformationUnstructured'],
-    ]),
-  ],
+  ['payment', new Map(DEFAULT_FIELD_MAPPING)],
+  ['deposit', new Map(DEFAULT_FIELD_MAPPING)],
 ]);
packages/desktop-client/src/components/banksync/AccountRow.tsx (3)

13-18: Consider moving tsToString to a shared utility.

The tsToString function could be useful in other components that need to format timestamps. Consider moving it to a shared utility file.


45-46: Use optional chaining for event handlers.

The onHover callback checks could be simplified using optional chaining.

-        onMouseEnter={() => onHover && onHover(account.id)}
-        onMouseLeave={() => onHover && onHover(null)}
+        onMouseEnter={() => onHover?.(account.id)}
+        onMouseLeave={() => onHover?.(null)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 45-45: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 46-46: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


75-87: Consider extracting button cell to a separate component.

The conditional rendering of edit/link buttons could be extracted to a separate component to improve readability and reusability.

type ActionButtonProps = {
  account: AccountEntity;
  onAction: AccountRowProps['onAction'];
};

const ActionButton: React.FC<ActionButtonProps> = ({ account, onAction }) => {
  if (account.account_sync_source) {
    return (
      <Cell name="edit" plain style={{ paddingRight: '10px' }}>
        <Button onPress={() => onAction(account, 'edit')}>
          <Trans>Edit</Trans>
        </Button>
      </Cell>
    );
  }
  return (
    <Cell name="link" plain style={{ paddingRight: '10px' }}>
      <Button onPress={() => onAction(account, 'link')}>
        <Trans>Link account</Trans>
      </Button>
    </Cell>
  );
};
packages/desktop-client/src/components/banksync/FieldMapping.tsx (2)

16-31: Consider moving hook to a separate file.

The useTransactionDirectionOptions hook could be moved to a separate hooks file for better organization and reusability.


85-152: Extract field row to a separate component.

The field mapping row rendering logic is complex and could be extracted to a separate component for better maintainability.

type FieldRowProps = {
  field: MappableFieldWithExample;
  mapping: Map<string, string>;
  setMapping: (field: string, value: string) => void;
};

const FieldRow: React.FC<FieldRowProps> = ({ field, mapping, setMapping }) => {
  const { t } = useTranslation();
  
  return (
    <Row
      key={field.actualField}
      style={{
        fontSize: 13,
        backgroundColor: theme.tableRowBackgroundHover,
        display: 'flex',
        alignItems: 'center',
        border: '1px solid var(--color-tableBorder)',
        minHeight: '40px',
      }}
      collapsed={true}
    >
      {/* ... rest of the row content ... */}
    </Row>
  );
};
packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (2)

42-66: Add documentation for sync fields.

Consider adding JSDoc comments to explain the purpose and expected format of each sync field, especially for fields like remittanceInformationUnstructured and remittanceInformationStructured.


135-141: Add loading state for preference updates.

The onSave function updates multiple preferences but doesn't handle the loading state or potential failures.

Consider this improvement:

   const onSave = async (close: () => void) => {
+    try {
+      setIsSaving(true);
       const mappingsStr = mappingsToString(mappings);
-      setSavedMappings(mappingsStr);
-      setSavedImportPending(String(importPending));
-      setSavedImportNotes(String(importNotes));
+      await Promise.all([
+        setSavedMappings(mappingsStr),
+        setSavedImportPending(String(importPending)),
+        setSavedImportNotes(String(importNotes))
+      ]);
       close();
+    } catch (error) {
+      console.error('Failed to save preferences:', error);
+      // Show error notification to user
+    } finally {
+      setIsSaving(false);
+    }
   };
packages/loot-core/src/server/aql/schema/index.ts (1)

56-57: Add documentation for new schema fields.

Consider adding comments to explain:

  • The purpose and format of raw_synced_data in transactions
  • The format and timezone of last_sync in accounts

Also applies to: 77-77

packages/loot-core/migrations/1738072673000_bank_sync_page.sql (1)

1-7: Consider enhancing the schema with constraints and indexes.

While the migration is functionally correct, consider these improvements:

  1. Add a default value (NULL) for new columns
  2. Add an index on last_sync if it will be frequently queried
  3. Consider adding CHECK constraints if there are valid format requirements for these fields
BEGIN TRANSACTION;

-ALTER TABLE accounts ADD COLUMN last_sync text;
+ALTER TABLE accounts ADD COLUMN last_sync text DEFAULT NULL;
+CREATE INDEX idx_accounts_last_sync ON accounts(last_sync);

-ALTER TABLE transactions ADD COLUMN raw_synced_data text;
+ALTER TABLE transactions ADD COLUMN raw_synced_data text DEFAULT NULL;

COMMIT;
packages/loot-core/src/types/models/account.d.ts (1)

13-14: Maintain consistent field naming convention.

The field naming is inconsistent:

  • bankName and bankId use camelCase
  • last_sync uses snake_case

Based on the retrieved learning, we should maintain consistency with server field names. Additionally, consider adding JSDoc comments to document the purpose of these fields.

-  bankName: T extends true ? string : null;
-  bankId: T extends true ? number : null;
+  /** Name of the bank as provided by the sync provider */
+  bank_name: T extends true ? string : null;
+  /** Unique identifier for the bank in the provider's system */
+  bank_id: T extends true ? number : null;
   mask: T extends true ? string : null; // end of bank account number
   official_name: T extends true ? string : null;
   balance_current: T extends true ? number : null;
   balance_available: T extends true ? number : null;
   balance_limit: T extends true ? number : null;
   account_sync_source: T extends true ? AccountSyncSource : null;
+  /** Timestamp of the last successful sync operation */
   last_sync: T extends true ? string : null;

Also applies to: 21-21

packages/loot-core/src/client/accounts/accountsSlice.ts (1)

193-194: Consider optimizing the account refresh.

While refreshing accounts after sync is necessary, consider debouncing multiple sync responses to prevent unnecessary re-renders when batch syncing accounts.

+import { debounce } from '../../shared/util';
+
+const debouncedGetAccounts = debounce((dispatch) => {
+  dispatch(getAccounts());
+}, 1000);
+
 function handleSyncResponse(
   accountId: AccountEntity['id'],
   res: SyncResponse,
   dispatch: AppDispatch,
   resNewTransactions: Array<TransactionEntity['id']>,
   resMatchedTransactions: Array<TransactionEntity['id']>,
   resUpdatedAccounts: Array<AccountEntity['id']>,
 ) {
   // ... existing code ...
-  dispatch(getAccounts());
+  debouncedGetAccounts(dispatch);
packages/loot-core/src/server/accounts/sync.ts (2)

353-355: Consider adding a preference for cleared status.

Currently, the code hardcodes the cleared status based on booked. Consider making this configurable like importPending and importNotes.


394-395: Sanitize notes before storage.

The code replaces '#' with '##' but should consider other potential markdown characters that might need escaping.

-        notes: importNotes ? notes.trim().replace('#', '##') : null,
+        notes: importNotes && notes ? sanitizeMarkdown(notes.trim()) : null,

Consider adding a utility function:

function sanitizeMarkdown(text: string): string {
  return text
    .replace(/#/g, '##')
    .replace(/\*/g, '\\*')
    .replace(/_/g, '\\_');
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1c3b9b and 3dc37f1.

⛔ Files ignored due to path filters (26)
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-4-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-5-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-6-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-13-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-14-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-15-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png is excluded by !**/*.png
  • packages/loot-core/src/server/__snapshots__/main.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap is excluded by !**/*.snap
  • upcoming-release-notes/4253.md is excluded by !**/*.md
📒 Files selected for processing (22)
  • packages/desktop-client/src/components/FinancesApp.tsx (2 hunks)
  • packages/desktop-client/src/components/Modals.tsx (2 hunks)
  • packages/desktop-client/src/components/banksync/AccountRow.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/AccountsHeader.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/AccountsList.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/FieldMapping.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/index.tsx (1 hunks)
  • packages/desktop-client/src/components/common/Input.tsx (1 hunks)
  • packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx (3 hunks)
  • packages/loot-core/migrations/1738072673000_bank_sync_page.sql (1 hunks)
  • packages/loot-core/src/client/accounts/accountsSlice.ts (1 hunks)
  • packages/loot-core/src/client/state-types/modals.d.ts (1 hunks)
  • packages/loot-core/src/mocks/index.ts (3 hunks)
  • packages/loot-core/src/server/accounts/sync.ts (3 hunks)
  • packages/loot-core/src/server/aql/schema/index.ts (2 hunks)
  • packages/loot-core/src/server/main.ts (4 hunks)
  • packages/loot-core/src/server/util/custom-sync-mapping.ts (1 hunks)
  • packages/loot-core/src/types/models/account.d.ts (1 hunks)
  • packages/loot-core/src/types/models/bank-sync.d.ts (1 hunks)
  • packages/loot-core/src/types/models/transaction.d.ts (1 hunks)
  • packages/loot-core/src/types/prefs.d.ts (1 hunks)
🧰 Additional context used
📓 Learnings (6)
packages/loot-core/src/types/models/account.d.ts (1)
Learnt from: matt-fidd
PR: actualbudget/actual#3581
File: packages/loot-core/src/types/models/bank-sync.d.ts:11-21
Timestamp: 2024-11-10T16:45:25.627Z
Learning: In `packages/loot-core/src/types/models/bank-sync.d.ts`, when defining TypeScript types for data received from the server, maintain the field names as they are in the server response, even if they don't follow TypeScript naming conventions.
packages/loot-core/src/types/prefs.d.ts (1)
Learnt from: MatissJanis
PR: actualbudget/actual#3458
File: packages/loot-core/src/client/state-types/prefs.d.ts:5-5
Timestamp: 2024-11-10T16:45:25.627Z
Learning: In future reviews, ensure that changes related to `PrefsState` in `prefs.d.ts` do not incorrectly assume necessary updates in other parts of the codebase. Verify the impact thoroughly before making suggestions.
packages/desktop-client/src/components/banksync/AccountRow.tsx (2)
Learnt from: matt-fidd
PR: actualbudget/actual#4253
File: packages/desktop-client/src/components/banksync/AccountRow.tsx:0-0
Timestamp: 2025-01-28T23:24:32.957Z
Learning: In the Actual codebase, timestamp formatting should use date-fns' `format` function with explicit format strings (e.g., 'HH:mm:ss' for time) instead of JavaScript's built-in `toLocaleTimeString()` for consistency.
Learnt from: joel-jeremy
PR: actualbudget/actual#3685
File: packages/desktop-client/src/components/accounts/Account.tsx:655-665
Timestamp: 2024-11-10T16:45:31.225Z
Learning: The Account component in 'packages/desktop-client/src/components/accounts/Account.tsx' is being rewritten in a separate PR.
packages/desktop-client/src/components/banksync/AccountsList.tsx (1)
Learnt from: joel-jeremy
PR: actualbudget/actual#3685
File: packages/desktop-client/src/components/accounts/Account.tsx:655-665
Timestamp: 2024-11-10T16:45:31.225Z
Learning: The Account component in 'packages/desktop-client/src/components/accounts/Account.tsx' is being rewritten in a separate PR.
packages/desktop-client/src/components/banksync/AccountsHeader.tsx (1)
Learnt from: joel-jeremy
PR: actualbudget/actual#3685
File: packages/desktop-client/src/components/accounts/Account.tsx:655-665
Timestamp: 2024-11-10T16:45:31.225Z
Learning: The Account component in 'packages/desktop-client/src/components/accounts/Account.tsx' is being rewritten in a separate PR.
packages/loot-core/src/types/models/bank-sync.d.ts (1)
Learnt from: matt-fidd
PR: actualbudget/actual#3581
File: packages/loot-core/src/types/models/bank-sync.d.ts:11-21
Timestamp: 2024-11-10T16:45:25.627Z
Learning: In `packages/loot-core/src/types/models/bank-sync.d.ts`, when defining TypeScript types for data received from the server, maintain the field names as they are in the server response, even if they don't follow TypeScript naming conventions.
🪛 Biome (1.9.4)
packages/desktop-client/src/components/banksync/AccountRow.tsx

[error] 45-45: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 46-46: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (20)
packages/loot-core/src/server/main.ts (3)

1048-1048: Adopt consistent async patterns.

Changing handleSyncResponse to an async function ensures you can await database updates. Ensure that all callers also leverage await or handle promise rejections as needed to avoid unhandled promise rejections.


1063-1066: Validate the last sync timestamp value.

You’re converting a timestamp to string before persisting. If future logic depends on strict numeric fields, consider storing numeric timestamps instead or clarifying in docs that last_sync is a text field.


1203-1203: Await handleSyncResponse in batch flows.

Now that handleSyncResponse is async, awaiting each handle ensures transactions and timestamps are updated before proceeding to subsequent operations. This is good for data consistency.

packages/desktop-client/src/components/banksync/AccountsHeader.tsx (1)

1-8: LGTM! Clean imports and type definition.

The imports are well-organized, and the type definition is clear and concise.

packages/desktop-client/src/components/banksync/AccountsList.tsx (1)

1-14: LGTM! Well-structured types and imports.

The type definitions are clear and follow TypeScript best practices. The action type is properly constrained using a union type.

packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx (1)

9-9: LGTM! Clean integration of bank sync navigation.

The changes follow the existing patterns and are well-integrated into the sidebar navigation.

Also applies to: 30-30, 67-72

packages/loot-core/src/types/prefs.d.ts (1)

31-33: LGTM! The new preference types follow established patterns.

The new string literal types for sync-related preferences maintain consistency with existing naming conventions and provide granular control over bank sync features.

Let's verify the naming consistency across the codebase:

✅ Verification successful

Naming consistency verified across the codebase

The preference key patterns are consistently implemented in both server and client code, maintaining the established naming conventions throughout the application.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for similar preference key patterns
rg "custom-sync|sync-import" --type ts

Length of output: 1130

packages/loot-core/src/mocks/index.ts (2)

43-43: LGTM! Timestamp stored as string follows schema design.

The implementation correctly stores the last_sync timestamp as a string, which aligns with the database schema design.


54-62: LGTM! Empty sync fields are properly initialized.

The emptySyncFields function correctly initializes all sync-related fields to null, maintaining consistency with the database schema.

packages/desktop-client/src/components/banksync/FieldMapping.tsx (1)

141-149: Add error handling for missing field mappings.

The example value lookup could fail silently if the mapping is not found. Consider adding error handling or a fallback value.

const example = field.syncFields.find(
  f => f.field === mapping.get(field.actualField),
)?.example ?? t('No example available');
packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (2)

1-25: LGTM! Well-structured type definitions and imports.

The type definitions and imports are well-organized, following TypeScript best practices.


124-133: LGTM! Proper error handling for JSON parsing.

The JSON parsing is properly wrapped in a try-catch block, following the previous review comment's suggestion.

packages/loot-core/src/client/state-types/modals.d.ts (1)

189-193: LGTM! Well-typed modal definition.

The new modal type is properly typed with the required account property.

packages/desktop-client/src/components/FinancesApp.tsx (1)

27-27: LGTM! Proper route integration.

The bank sync route is properly integrated into the existing routing structure.

Also applies to: 252-252

packages/desktop-client/src/components/Modals.tsx (1)

14-14: LGTM! Modal integration looks good.

The new EditSyncAccount modal is properly integrated into the modal system, following the established patterns for modal rendering and prop passing.

Also applies to: 388-390

packages/loot-core/src/types/models/bank-sync.d.ts (1)

22-23: LGTM! Type definition is clear and follows conventions.

The BankSyncProviders type appropriately defines the supported providers using a union type.

packages/desktop-client/src/components/common/Input.tsx (1)

57-59: LGTM! Good UX improvement.

The conditional styling for disabled state provides better visual feedback to users.

packages/loot-core/src/server/accounts/sync.ts (3)

329-345: Good use of Promise.all for parallel queries.

The code efficiently fetches multiple preferences in parallel, improving performance.


399-400: Good practice: Storing raw sync data.

Storing the original transaction data enables better debugging and future data recovery if needed.


347-349: Verify mapping string parsing.

Ensure that mappingsFromString properly handles invalid mapping strings to prevent runtime errors.

✅ Verification successful

✓ Mapping string parsing is properly implemented with error handling

The mappingsFromString function in custom-sync-mapping.ts includes comprehensive error handling for invalid inputs:

  • JSON parsing errors
  • Type validation
  • Null checks
  • Descriptive error messages
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for error handling in mappingsFromString
ast-grep --pattern 'function mappingsFromString($_) {
  $$$
  try {
    $$$
  } catch {
    $$$
  }
}'

Length of output: 104


Script:

#!/bin/bash
# Find mappingsFromString function definition
ast-grep --pattern 'mappingsFromString = $_'

# Also try const/let/var declaration
ast-grep --pattern 'const mappingsFromString = $_'

# Search for the function name in the codebase
rg "mappingsFromString" -A 5

Length of output: 10817

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@matt-fidd
Copy link
Contributor Author

/update-vrt

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (2)
packages/loot-core/src/server/main.ts (1)

Line range hint 1048-1066: Add TypeScript types and improve error handling.

The function implementation could be improved in several areas:

  1. Add TypeScript types for parameters to improve type safety
  2. Add error handling for the database update
  3. Consider storing the timestamp as an INTEGER in the database for better sorting and querying
  4. Consider returning the modified arrays instead of mutating external state
-async function handleSyncResponse(
-  res,
-  acct,
-  newTransactions,
-  matchedTransactions,
-  updatedAccounts,
-) {
+async function handleSyncResponse(
+  res: { added: Transaction[]; updated: Transaction[] },
+  acct: Account,
+  newTransactions: Transaction[],
+  matchedTransactions: Transaction[],
+  updatedAccounts: string[],
+): Promise<void> {
   const { added, updated } = res;

   newTransactions.push(...added);
   matchedTransactions.push(...updated);

   if (added.length > 0) {
     updatedAccounts.push(acct.id);
   }

   const ts = new Date().getTime().toString();
   const id = acct.id;
-  await db.runQuery(`UPDATE accounts SET last_sync = ? WHERE id = ?`, [ts, id]);
+  try {
+    await db.runQuery(
+      `UPDATE accounts SET last_sync = ? WHERE id = ?`,
+      [ts, id]
+    );
+  } catch (error) {
+    logger.error('Failed to update last_sync timestamp:', error);
+    throw new Error('Failed to update sync timestamp');
+  }
 }
packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (1)

Line range hint 394-395: Add null check for notes trimming.

The notes.trim() call could throw if notes is null or undefined.

Apply this diff to add proper null checking:

-        notes: importNotes ? notes.trim().replace('#', '##') : null,
+        notes: importNotes && notes ? notes.trim().replace('#', '##') : null,
♻️ Duplicate comments (2)
packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (1)

173-179: ⚠️ Potential issue

Remove unsafe non-null assertion.

The non-null assertion on mapping! could lead to runtime errors if mapping is undefined.

Apply this diff to improve type safety:

-              mapping={mapping!}
+              mapping={mapping ?? new Map()}
packages/loot-core/src/server/accounts/sync.ts (1)

361-366: ⚠️ Potential issue

Add validation for mapped fields.

The code should validate that the mapped fields exist in the transaction object before accessing them.

Apply this diff to add proper validation:

-    const mapping = mappings.get(trans.amount <= 0 ? 'payment' : 'deposit');
-
-    const date = trans[mapping.get('date')] ?? trans.date;
-    const payeeName = trans[mapping.get('payee')];
-    const notes = trans[mapping.get('notes')];
+    const direction = trans.amount <= 0 ? 'payment' : 'deposit';
+    const mapping = mappings.get(direction);
+    
+    if (!mapping) {
+      throw new Error(`No mapping found for direction: ${direction}`);
+    }
+    
+    const dateField = mapping.get('date');
+    const payeeField = mapping.get('payee');
+    const notesField = mapping.get('notes');
+    
+    if (!dateField || !payeeField) {
+      throw new Error('Required mapping fields (date, payee) are missing');
+    }
+    
+    const date = trans[dateField] ?? trans.date;
+    const payeeName = trans[payeeField];
+    const notes = notesField ? trans[notesField] : null;
🧹 Nitpick comments (9)
packages/desktop-client/e2e/page-models/bank-sync-page.js (1)

1-10: Consider converting to TypeScript for better type safety.

The page model would benefit from TypeScript conversion to match other files in the codebase and provide better type safety for the page and options parameters.

-export class BankSyncPage {
+export class BankSyncPage {
+  private page: Page;
+  private bankSync: Locator;
+
   constructor(page) {
     this.page = page;
     this.bankSync = page.getByTestId('banksync');
   }

-  async waitFor(options) {
+  async waitFor(options?: WaitForOptions): Promise<void> {
     await this.bankSync.waitFor(options);
   }
 }
packages/desktop-client/e2e/page-models/navigation.js (1)

59-70: Extract duplicated menu expansion logic.

The menu expansion logic is duplicated across multiple navigation methods. Consider extracting it into a reusable helper method.

+  private async expandMoreMenuIfNeeded(link: Locator): Promise<void> {
+    if (!(await link.isVisible())) {
+      await this.page.getByRole('button', { name: 'More' }).click();
+    }
+  }

   async goToBankSyncPage() {
     const bankSyncLink = this.page.getByRole('link', { name: 'Bank Sync' });
-    if (!(await bankSyncLink.isVisible())) {
-      await this.page.getByRole('button', { name: 'More' }).click();
-    }
+    await this.expandMoreMenuIfNeeded(bankSyncLink);
     await bankSyncLink.click();
     return new BankSyncPage(this.page);
   }
packages/desktop-client/src/components/banksync/AccountRow.tsx (4)

12-17: Move timestamp formatting to a shared utility.

The tsToString function could be useful in other components. Consider moving it to a shared utility file.

// New file: src/utils/dateFormatting.ts
+import { format } from 'loot-core/src/shared/months';
+
+export const formatTimestamp = (ts: string | null, dateFormat: string): string => {
+  if (!ts) return 'Unknown';
+  const parsed = new Date(parseInt(ts, 10));
+  return `${format(parsed, dateFormat)} ${format(parsed, 'HH:mm:ss')}`;
+};

44-45: Use optional chaining for event handlers.

Replace the conditional checks with optional chaining for cleaner code.

-        onMouseEnter={() => onHover && onHover(account.id)}
-        onMouseLeave={() => onHover && onHover(null)}
+        onMouseEnter={() => onHover?.(account.id)}
+        onMouseLeave={() => onHover?.(null)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 44-44: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 45-45: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


47-72: Extract common cell styles to reduce duplication.

The cell styling is duplicated across multiple cells. Consider extracting it to a constant or styled component.

+const commonCellStyle = {
+  color: theme.tableText,
+  padding: '10px',
+};

 <Cell
   name="accountName"
   width={250}
   plain
-  style={{ color: theme.tableText, padding: '10px' }}
+  style={commonCellStyle}
 >

74-86: Add aria-label to improve accessibility.

The Edit and Link buttons should have descriptive aria-labels for better accessibility.

-            <Button onPress={() => onAction(account, 'edit')}>
+            <Button 
+              onPress={() => onAction(account, 'edit')}
+              aria-label={`Edit ${account.name} sync settings`}
+            >
               <Trans>Edit</Trans>
             </Button>

-            <Button onPress={() => onAction(account, 'link')}>
+            <Button 
+              onPress={() => onAction(account, 'link')}
+              aria-label={`Link ${account.name} to bank account`}
+            >
               <Trans>Link account</Trans>
             </Button>
packages/desktop-client/src/components/banksync/FieldMapping.tsx (3)

4-5: Consider consolidating icon imports to use a single version.

Icons are imported from two different versions (v0 and v1). For consistency, consider using icons from the same version.


16-31: Consider extracting transaction direction options.

The transaction direction options could be defined outside the hook as a constant to improve reusability and reduce re-renders.

+const TRANSACTION_DIRECTIONS = [
+  {
+    value: 'payment',
+    label: 'Payment',
+  },
+  {
+    value: 'deposit',
+    label: 'Deposit',
+  },
+] as const;
+
 const useTransactionDirectionOptions = () => {
   const { t } = useTranslation();
 
-  const transactionDirectionOptions = [
-    {
-      value: 'payment',
-      label: t('Payment'),
-    },
-    {
-      value: 'deposit',
-      label: t('Deposit'),
-    },
-  ];
+  const transactionDirectionOptions = TRANSACTION_DIRECTIONS.map(({ value, label }) => ({
+    value,
+    label: t(label),
+  }));
 
   return { transactionDirectionOptions };
 };

85-152: Consider memoizing mapped fields for better performance.

The field mapping operation could be memoized to prevent unnecessary re-renders, especially for large datasets.

+import { useMemo } from 'react';

 export function FieldMapping({
   transactionDirection,
   setTransactionDirection,
   fields,
   mapping,
   setMapping,
 }: FieldMappingProps) {
   const { t } = useTranslation();
   const { transactionDirectionOptions } = useTransactionDirectionOptions();
 
+  const mappedFields = useMemo(
+    () =>
+      fields.map(field => ({
+        key: field.actualField,
+        mappedValue: mapping.get(field.actualField),
+        example: field.syncFields.find(
+          f => f.field === mapping.get(field.actualField),
+        )?.example,
+      })),
+    [fields, mapping],
+  );
 
   return (
     <>
       <Select /* ... */ />
       <TableHeader>/* ... */</TableHeader>
-      {fields.map(field => {
+      {mappedFields.map(({ key, mappedValue, example }) => {
         return (
           <Row
-            key={field.actualField}
+            key={key}
             /* ... */
           >
             /* ... */
           </Row>
         );
       })}
     </>
   );
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3dc37f1 and ad9e50a.

⛔ Files ignored due to path filters (26)
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-checks-the-page-visuals-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-4-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-5-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-6-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/rules.test.ts-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-13-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-14-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/schedules.test.ts-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-15-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/settings.test.ts-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png is excluded by !**/*.png
  • packages/loot-core/src/server/__snapshots__/main.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap is excluded by !**/*.snap
  • packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap is excluded by !**/*.snap
  • upcoming-release-notes/4253.md is excluded by !**/*.md
📒 Files selected for processing (25)
  • packages/desktop-client/e2e/bank-sync.test.ts (1 hunks)
  • packages/desktop-client/e2e/page-models/bank-sync-page.js (1 hunks)
  • packages/desktop-client/e2e/page-models/navigation.js (2 hunks)
  • packages/desktop-client/src/components/FinancesApp.tsx (2 hunks)
  • packages/desktop-client/src/components/Modals.tsx (2 hunks)
  • packages/desktop-client/src/components/banksync/AccountRow.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/AccountsHeader.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/AccountsList.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/FieldMapping.tsx (1 hunks)
  • packages/desktop-client/src/components/banksync/index.tsx (1 hunks)
  • packages/desktop-client/src/components/common/Input.tsx (1 hunks)
  • packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx (3 hunks)
  • packages/loot-core/migrations/1738072673000_bank_sync_page.sql (1 hunks)
  • packages/loot-core/src/client/accounts/accountsSlice.ts (1 hunks)
  • packages/loot-core/src/client/state-types/modals.d.ts (1 hunks)
  • packages/loot-core/src/mocks/index.ts (3 hunks)
  • packages/loot-core/src/server/accounts/sync.ts (3 hunks)
  • packages/loot-core/src/server/aql/schema/index.ts (2 hunks)
  • packages/loot-core/src/server/main.ts (4 hunks)
  • packages/loot-core/src/server/util/custom-sync-mapping.ts (1 hunks)
  • packages/loot-core/src/types/models/account.d.ts (1 hunks)
  • packages/loot-core/src/types/models/bank-sync.d.ts (1 hunks)
  • packages/loot-core/src/types/models/transaction.d.ts (1 hunks)
  • packages/loot-core/src/types/prefs.d.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (17)
  • packages/loot-core/src/types/models/bank-sync.d.ts
  • packages/loot-core/src/types/models/transaction.d.ts
  • packages/desktop-client/src/components/common/Input.tsx
  • packages/loot-core/migrations/1738072673000_bank_sync_page.sql
  • packages/desktop-client/src/components/sidebar/PrimaryButtons.tsx
  • packages/desktop-client/src/components/FinancesApp.tsx
  • packages/loot-core/src/client/state-types/modals.d.ts
  • packages/loot-core/src/types/prefs.d.ts
  • packages/loot-core/src/server/aql/schema/index.ts
  • packages/loot-core/src/client/accounts/accountsSlice.ts
  • packages/desktop-client/src/components/banksync/AccountsHeader.tsx
  • packages/loot-core/src/types/models/account.d.ts
  • packages/desktop-client/src/components/Modals.tsx
  • packages/desktop-client/src/components/banksync/AccountsList.tsx
  • packages/loot-core/src/server/util/custom-sync-mapping.ts
  • packages/loot-core/src/mocks/index.ts
  • packages/desktop-client/src/components/banksync/index.tsx
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/banksync/AccountRow.tsx (2)
Learnt from: matt-fidd
PR: actualbudget/actual#4253
File: packages/desktop-client/src/components/banksync/AccountRow.tsx:0-0
Timestamp: 2025-01-28T23:24:32.957Z
Learning: In the Actual codebase, timestamp formatting should use date-fns' `format` function with explicit format strings (e.g., 'HH:mm:ss' for time) instead of JavaScript's built-in `toLocaleTimeString()` for consistency.
Learnt from: joel-jeremy
PR: actualbudget/actual#3685
File: packages/desktop-client/src/components/accounts/Account.tsx:655-665
Timestamp: 2024-11-10T16:45:31.225Z
Learning: The Account component in 'packages/desktop-client/src/components/accounts/Account.tsx' is being rewritten in a separate PR.
🪛 Biome (1.9.4)
packages/desktop-client/src/components/banksync/AccountRow.tsx

[error] 44-44: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 45-45: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Visual regression
  • GitHub Check: Functional
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (3)
packages/loot-core/src/server/main.ts (2)

1134-1134: LGTM! Well-structured error handling.

The usage of handleSyncResponse in the bank sync handler is well implemented with proper error handling and logging.


1203-1203: LGTM! Well-structured error handling.

The usage of handleSyncResponse in the SimpleFin batch sync handler is well implemented with proper error handling and logging.

packages/desktop-client/src/components/banksync/FieldMapping.tsx (1)

33-39: LGTM! Well-structured type definitions.

The props interface is clear, properly typed, and follows TypeScript best practices.

packages/desktop-client/e2e/bank-sync.test.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (1)

108-113: Improve boolean state initialization.

The string comparison for boolean state initialization could be simplified using a utility function.

+const toBool = (value: string | boolean) => String(value) === 'true';
+
 const [importPending, setImportPending] = useState(
-  String(savedImportPending) === 'true',
+  toBool(savedImportPending)
 );
 const [importNotes, setImportNotes] = useState(
-  String(savedImportNotes) === 'true',
+  toBool(savedImportNotes)
 );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad9e50a and a485534.

📒 Files selected for processing (2)
  • packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (1 hunks)
  • packages/loot-core/src/server/util/custom-sync-mapping.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/loot-core/src/server/util/custom-sync-mapping.ts
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Visual regression
  • GitHub Check: Functional
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-latest)
  • GitHub Check: Analyze
🔇 Additional comments (3)
packages/desktop-client/src/components/banksync/EditSyncAccount.tsx (3)

26-40: LGTM! Well-structured type definitions.

The type definitions are clear, properly exported, and provide good type safety for the component.


79-88: Improve type safety in field mapping.

The type assertion syncField as keyof TransactionEntity could lead to runtime errors if syncField is not actually a key of TransactionEntity.


182-188: Remove unsafe non-null assertion.

The non-null assertion on mapping! could lead to runtime errors if mapping is undefined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
1 participant